package com.spzx.product.service.impl;

import com.spzx.common.core.utils.StringUtils;
import com.spzx.product.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class TestServiceImpl implements TestService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 本地锁测试
     * 单台节点没有问题
     * 集群模式会出现数据不一致
     */
    public synchronized void testLockV1() {
        // 查询Redis中的num值
        String value = this.stringRedisTemplate.opsForValue().get("num");
        // 没有该值return
        if (StringUtils.isBlank(value)) {
            return;
        }
        // 有值就转成int
        int num = Integer.parseInt(value);
        // 把Redis中的num值+1
        this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
    }

    /**
     * 分布式锁测试第一版
     * 将分布式锁添加到Redis中，使用Redis中的setnx命令添加分布式锁，在StringRedisTemplate中对应的方法是：setIfAbsent方法
     * 存在的问题：
     * 当业务逻辑执行出现异常时会出现死锁现象
     */
    public void testLockV2(String port) {
        //获取Redis中的分布式锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", "helloRedis");
        if (flag) {
            System.out.println("获取分布式锁成功");
            // 查询Redis中的num值
            String value = this.stringRedisTemplate.opsForValue().get("num");
            // 没有该值return
            if (StringUtils.isBlank(value)) {
                return;
            }

            //如果请求的是9205这台机器则出异常
            if ("9205".equals(port)) {
                //设置数学异常
                System.out.println(1 / 0);
            }

            // 有值就转成int
            int num = Integer.parseInt(value);
            // 把Redis中的num值+1
            this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));

            //执行完业务逻辑，一定要释放锁
            stringRedisTemplate.delete("lock");

        } else {
            System.err.println("获取分布式锁失败：重试");
            try {
                //睡1秒再重试
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //重试，再次调用testLock方法尝试获取分布式锁
            testLock(port);
        }

    }


    /**
     * 分布式锁测试第二版
     * 解决死锁问题：
     * 1.使用try catch finally解决死锁问题
     */
    public void testLockV3(String port) {
        //获取Redis中的分布式锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", "helloRedis");
        if (flag) {
            try {
                System.out.println("获取分布式锁成功");
                // 查询Redis中的num值
                String value = this.stringRedisTemplate.opsForValue().get("num");
                // 没有该值return
                if (StringUtils.isBlank(value)) {
                    return;
                }

                //如果请求的是9205这台机器则出异常
//               if("9215".equals(port)){
//                   //设置数学异常
//                   System.out.println(1/0);
//               }

                // 有值就转成int
                int num = Integer.parseInt(value);
                // 把Redis中的num值+1
                this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));


            } catch (NumberFormatException e) {
                System.err.println("出错啦：" + e.getMessage());
            } finally {
                //执行完业务逻辑不管是否出现异常，一定要释放锁
                stringRedisTemplate.delete("lock");
            }

        } else {
            System.err.println("获取分布式锁失败：重试");
            try {
                //睡1秒再重试
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //重试，再次调用testLock方法尝试获取分布式锁
            testLock(port);
        }

    }

    /**
     * 分布式锁测试
     * 解决死锁问题：
     * 2.给分布式锁设置过期时间
     * 有可能出现的问题：
     * 1.如果业务逻辑执行的时间比锁的过期时间要长相对于没有枷锁
     * 2.有可能释放别人的锁
     */
    public void testLockV4(String port) {
        //获取Redis中的分布式锁，并设置分布式锁的过期时间
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", "helloRedis", 10, TimeUnit.MILLISECONDS);

        if (flag) {
            System.out.println("获取分布式锁成功");
            //业务逻辑执行超过30毫秒
            try {
                Thread.sleep(25);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 查询Redis中的num值
            String value = this.stringRedisTemplate.opsForValue().get("num");
            // 没有该值return
            if (StringUtils.isBlank(value)) {
                return;
            }

            //如果请求的是9205这台机器则出异常
//            if ("9215".equals(port)) {
//                //设置数学异常
//                System.out.println(1 / 0);
//            }


            // 有值就转成int
            int num = Integer.parseInt(value);
            // 把Redis中的num值+1
            this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));

            //执行完业务逻辑不管是否出现异常，一定要释放锁
            stringRedisTemplate.delete("lock");

        } else {
            System.err.println("获取分布式锁失败：重试");
            try {
                //睡1秒再重试
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //重试，再次调用testLock方法尝试获取分布式锁
            testLock(port);
        }

    }

    /*解决死锁问题：
     * 2.给分布式锁设置过期时间
     *  解决释放别的线程锁的问题：
     *      给锁设置不同的值，释放锁的时候判断是否是自己上的锁
     *      结论：还是会释放别人的锁
     *
     */
    public void testLockV5(String port) {
        //使用UUID随机生成一个字符串作为锁的值
        String uuid = UUID.randomUUID().toString().replaceAll("-","");
        //获取Redis中的分布式锁，并设置分布式锁的过期时间
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.MILLISECONDS);

        if (flag) {
            System.out.println("获取分布式锁成功");
            //业务逻辑执行超过30毫秒
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 查询Redis中的num值
            String value = this.stringRedisTemplate.opsForValue().get("num");
            // 没有该值return`
            if (StringUtils.isBlank(value)) {
                return;
            }

            // 有值就转成int
            int num = Integer.parseInt(value);
            // 把Redis中的num值+1
            this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));

            /*
                判断从Redis中获取的锁是否是自己上的锁
                由于一下代码缺乏原子性，所以虽然理论上解决了不释放别人分布式锁的问题，但是实际上还是会释放别人的锁，
                所以一下代码需要使用Lua脚本保证原子性
             */
            if(uuid.equals(this.stringRedisTemplate.opsForValue().get("lock"))){
                 //进入if之后睡觉，在睡觉的过程中分布式锁失效
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //释放自己上的分布式锁
                stringRedisTemplate.delete("lock");
            }

        } else {
            System.err.println("获取分布式锁失败：重试");
            try {
                //睡1秒再重试
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //重试，再次调用testLock方法尝试获取分布式锁
            testLock(port);
        }

    }

    /*解决死锁问题：
     * 2.给分布式锁设置过期时间
     *  解决释放别的线程锁的问题：
     *      使用Lua脚本
     *
     */

    public void testLockV6(String port) {
        //使用UUID随机生成一个字符串作为锁的值
        String uuid = UUID.randomUUID().toString().replaceAll("-","");
        //获取Redis中的分布式锁，并设置分布式锁的过期时间
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.MILLISECONDS);

        if (flag) {
            System.out.println("获取分布式锁成功");
            //业务逻辑执行超过30毫秒
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 查询Redis中的num值
            String value = this.stringRedisTemplate.opsForValue().get("num");
            // 没有该值return
            if (StringUtils.isBlank(value)) {
                return;
            }


            // 有值就转成int
            int num = Integer.parseInt(value);
            // 把Redis中的num值+1
            this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));

            //执行Lua脚本判断是否是自己上的锁及释放自己的锁
            //设置Lua脚本
            String luaScript = """
                    if redis.call('get', KEYS[1]) == ARGV[1] then
                        return redis.call('del', KEYS[1])
                    else
                        return 0
                    end
                    """;
            //执行Lua脚本
            Boolean lock = stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript, Boolean.class), Arrays.asList("lock"), uuid);

        } else {
            System.err.println("获取分布式锁失败：重试");
            try {
                //睡1秒再重试
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //重试，再次调用testLock方法尝试获取分布式锁
            testLock(port);
        }

    }

    /**
     * 可重入锁：上锁
     * @param lockName
     * @param uuid
     * @param expire
     * @return
     */
    private Boolean tryLock(String lockName, String uuid, Long expire){
        String script = """
        if(redis.call('exists' , KEYS[1]) == 0 ) then
            redis.call('hset' , KEYS[1] ,ARGV[1] , 1)
            redis.call('expire' , KEYS[1] , ARGV[2])
            return 1
        elseif(redis.call('hexists' , KEYS[1], ARGV[1]) == 1) then
            redis.call('hincrby' , KEYS[1] ,ARGV[1] , 1)
            return 1
        else
            return 0
        end
        """;

        if (!this.stringRedisTemplate.execute(
                new DefaultRedisScript<>(script, Boolean.class),
                Arrays.asList(lockName),
                uuid,
                expire.toString() //秒
        )){
            try {
                //睡眠
                Thread.sleep(100);
                //自旋重试
                System.out.println("重试");
                tryLock(lockName, uuid, expire);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 锁续期
        this.renewTime(lockName, uuid, expire);

        // 获取到锁，返回true
        return true;
    }


    /**
     * 可重入锁：解锁
     * @param lockName
     * @param uuid
     */
    private void unlock(String lockName, String uuid){

        String script = """
        if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
            return nil;
    	end;
    	if (redis.call('hincrby', KEYS[1], ARGV[1], -1) > 0) then
        	return 0;
    	else
       	 	redis.call('del', KEYS[1]);
   			return 1;
    	end;
    	""";
        Long result = stringRedisTemplate.execute(
                new DefaultRedisScript<>(script, Long.class),
                Arrays.asList(lockName),
                uuid
        );
        //其他进程正在持有锁
        if(result == null){
            throw new IllegalMonitorStateException("当前线程" + lockName + "." + uuid + "不持有锁");
        }
    }

    /**
     * 测试可重入锁
     *  上锁解锁使用的是Redis中Hash类型的数据结构
     */
    public void testLockV7(String port) {
        // 加锁
        String uuid = UUID.randomUUID().toString();
        //上可重入锁
        Boolean flag = this.tryLock("lock", uuid, 300L);//300秒，便于断点调试

        if (flag) {
            //获取锁成功，执行业务代码
            //1.先从redis中通过key num获取值  key提前手动设置 num 初始值：0
            String value = this.stringRedisTemplate.opsForValue().get("num");

            System.out.println("value = " + value);

            //2.如果值为空则非法直接返回即可
            if (StringUtils.isBlank(value)) {
                return;
            }

            //3.对num值进行自增加一
            Integer num = Integer.parseInt(value);
            this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));

            // 测试可重入性
            this.testSubLock(uuid);

            // 释放锁
            this.unlock("lock", uuid);
        }
    }

    // 测试可重入性
    private void testSubLock(String uuid){
        // 加锁
        Boolean lock = this.tryLock("lock", uuid, 300L);

        if (lock) {
            System.out.println("分布式可重入锁。。。");

            this.unlock("lock", uuid);
        }
    }


    /**
     * 锁延期方法
     * 线程等待超时时间的2/3时间后,执行锁延时代码,直到业务逻辑执行完毕,
     * 因此在此过程中,其他线程无法获取到锁,保证了线程安全性
     * @param lockName
     * @param expire 单位：毫秒
     */
    private void renewTime(String lockName, String uuid, Long expire){

        //执行lua脚本
        String script = """
        if(redis.call('hexists', KEYS[1], ARGV[1]) == 1)
            then redis.call('expire', KEYS[1], ARGV[2]);
            return 1;
        else
            return 0;
        end
    	""";

        //创建新线程，查看锁的持有时间是否超过整个生命周期的2/3
        new Thread(()->{
            do{
                try {
                    Thread.sleep(expire * 2 / 3 * 1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }while(stringRedisTemplate.execute(
                    new DefaultRedisScript<>(script, Boolean.class),
                    Arrays.asList(lockName),
                    uuid,
                    expire.toString()
            ));
        }).start();
    }

    /**
     * 测试锁延期
     */
    @Override
    public void testLock(String port) {
        // 加锁
        String uuid = UUID.randomUUID().toString();
        //上可重入锁
        Boolean flag = this.tryLock("lock", uuid, 30L);//300秒，便于断点调试

        if (flag) {
            //睡60秒
//            try {
//                Thread.sleep(60000);
//            } catch (InterruptedException e) {
//                throw new RuntimeException(e);
//            }
            //执行主程序出现异常
            System.out.println(9/0);
            //获取锁成功，执行业务代码
            //1.先从redis中通过key num获取值  key提前手动设置 num 初始值：0
            String value = this.stringRedisTemplate.opsForValue().get("num");

            System.out.println("value = " + value);

            //2.如果值为空则非法直接返回即可
            if (StringUtils.isBlank(value)) {
                return;
            }

            //3.对num值进行自增加一
            Integer num = Integer.parseInt(value);
            this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));

            // 释放锁
            this.unlock("lock", uuid);
        }
    }
}